﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using EnvDTE;
using System.Threading.Tasks;
using Util;
using Microsoft.VisualStudio.Shell.Interop;
using Logging;
using System.ComponentModel.Composition;
using Microsoft.VisualStudio.Text.Operations;
using System.Threading;

namespace RoslynProcess
{
    [Export]
    abstract class AbstractProcessHandler : IDisposable
    {
        private readonly string directory; // base directory of extension
        protected string Directory { get { return directory; } }

        private System.Diagnostics.Process p;

        private DTE dte { get; set; }
        private RunningDocumentTableHandler RunningDocTable { get; set; }

        protected abstract string PathToExecutable { get; }

        protected string CurrentlyLoadedSolution { get; private set; }
        protected StreamWriter ToProcess { get; private set; }
        // Warnings say to not lock on a StreamWriter.
        protected object WriteToProcessLock { get; private set; }
        protected Queue<string> FromProcess { get; private set; }
        private SemaphoreSlim FromProcessSemaphore { get; set; }
        private CancellationTokenSource FromProcessTokenSource { get; set; }

        public AbstractProcessHandler(string vsPackageGuid)
        {
            this.directory = VSUtil.GetThisExtensionDirectory(vsPackageGuid);
            Logger.SetOutputFile(new DirectoryInfo(this.directory)
                    .CreateSubdirectory("log").FullName
                    + @"\log." + DateTime.Now.ToString("s").Replace(":", "") + ".txt");
            Logger.PrintLineWithTime("Started extension");
            SetupProcess();
        }

        private void SetupProcess()
        {
            CurrentlyLoadedSolution = null;

            var oldFromProcess = FromProcess;
            if (oldFromProcess != null)
            {
                Monitor.Enter(oldFromProcess);
            }

            p = new System.Diagnostics.Process();
            p.StartInfo.FileName = PathToExecutable;
            p.StartInfo.Arguments = "\"" + directory + "\"";
            p.StartInfo.UseShellExecute = false;
            p.StartInfo.CreateNoWindow = true;
            p.StartInfo.RedirectStandardOutput = true;
            p.StartInfo.RedirectStandardInput = true;
            p.OutputDataReceived += new System.Diagnostics.DataReceivedEventHandler(p_OutputDataReceived);
            p.Exited += new EventHandler(p_Exited);
            p.Start();

            ToProcess = p.StandardInput;
            WriteToProcessLock = new object();
            p.BeginOutputReadLine();

            FromProcess = new Queue<string>();
            FromProcessSemaphore = new SemaphoreSlim(1, int.MaxValue);
            FromProcessTokenSource = new CancellationTokenSource();
            if (oldFromProcess != null)
            {
                // Make sure all threads watching the old queue aren't still waiting.
                Monitor.PulseAll(oldFromProcess);
                Monitor.Exit(oldFromProcess);
            }

            Logger.PrintLineWithTime("Started " + new FileInfo(PathToExecutable).Name);
        }

        void p_Exited(object sender, EventArgs e)
        {
            lock (FromProcess)
            {
                FromProcess.Clear();
                FromProcessTokenSource.Cancel();
            }
        }

        void p_OutputDataReceived(object sender, System.Diagnostics.DataReceivedEventArgs e)
        {
            lock (FromProcess)
            {
                if (e.Data == null)
                {
                    if (p.HasExited)
                    {
                        p_Exited(p, null);
                    }
                }
                else
                {
                    // XXX DEBUG
                    //Logger.PrintLineWithTime("Got line: " + e.Data);
                    if (!HandleLine(e.Data))
                    {
                        Logger.PrintLineWithTime("Unhandled line: " + e.Data);
                        FromProcess.Enqueue(e.Data);
                        FromProcessSemaphore.Release();
                    }
                }
            }
        }
        /// <summary>
        /// Handle incoming line from program asynchronously. Return false if line should be
        /// considered unread. Called while holding lock on reading from process, so should
        /// be handled quickly.
        /// </summary>
        /// <returns></returns>
        protected abstract bool HandleLine(string line);
        protected bool EnsureProcessRunning()
        {
            if (p.HasExited)
            {
                SetupProcess();
                return true;
            }
            else
            {
                return false;
            }
        }
        protected void AssertProcessRunning()
        {
            if (p.HasExited)
            {
                throw new Exception("Process not running.");
            }
        }
        protected bool IsProcessRunning
        {
            get
            {
                return !p.HasExited;
            }
        }

        public void Dispose()
        {
            p.Kill();
            p.Dispose();
            p = null;
            ToProcess = null;
            FromProcess = null;
        }

        public void LoadSolution(EnvDTE.DTE dte, string filename, bool reload = false)
        {
            bool restarted = EnsureProcessRunning();
            lock (WriteToProcessLock)
            {
                if (reload
                    || restarted
                    || CurrentlyLoadedSolution == null
                    || !filename.Equals(CurrentlyLoadedSolution))
                {
                    ToProcess.WriteLine("LoadSolution " + filename);
                    CurrentlyLoadedSolution = filename;
                    // TODO Right way to filter documents?
                    foreach (Document unsavedDoc in dte.Documents
                                                      .OfType<Document>()
                                                      .Where(d => d != null && !d.Saved))
                    {
                        SendDocument(unsavedDoc);
                    }
                }
            }
            // dte shouldn't change, so really this is just a check against null.
            if (dte != this.dte)
            {
                this.dte = dte;
                RunningDocTable = new RunningDocumentTableHandler(dte);
                RunningDocTable.OnBeforeDocumentWindowShow += OnBeforeDocumentWindowShow;
                RunningDocTable.OnAfterDocumentWindowHide += OnAfterDocumentWindowHide;
                RunningDocTable.OnAfterSave += OnAfterSave;
            }
            //ReadAllLines();
            Logger.PrintLineWithTime("Loaded solution: " + new FileInfo(filename).Name);
        }
        public void LoadCurrentSolution(EnvDTE.DTE dte)
        {
            LoadSolution(dte, dte.Solution.FullName);
        }

        private void ChooseProject(Project project)
        {
            AssertProcessRunning();
            lock (WriteToProcessLock)
            {
                ToProcess.WriteLine("ChooseProject " + project.Name);
                //ReadAllLines();
                Logger.PrintLineWithTime("Chose project: " + project.Name);
            }
        }

        private void ChooseDocument(Document doc)
        {
            AssertProcessRunning();
            lock (WriteToProcessLock)
            {
                ToProcess.WriteLine("ChooseDocument " + doc.Name);
                ToProcess.WriteLine("LoadDocument " + doc.FullName);
                //ReadAllLines();
                Logger.PrintLineWithTime("Chose document: " + doc.Name);
            }
        }

        public void ChooseDocumentAndProject(Document doc)
        {
            lock (WriteToProcessLock)
            {
                LoadCurrentSolution(doc.DTE);
                ChooseProject(doc.ProjectItem.ContainingProject);
                ChooseDocument(doc);
            }
        }

        public void FullyLoadDocument(Document doc, bool forceSend = false)
        {
            lock (WriteToProcessLock)
            {
                if (doc.Saved && !forceSend)
                {
                    ChooseDocumentAndProject(doc);
                }
                else
                {
                    SendDocument(doc);
                }
            }
        }

        public void SendDocument(Document doc)
        {
            string text = doc.GetIWpfTextView().TextSnapshot.GetText();
            string[] lines = text.Split(new String[] { "\r\n", "\n", "\r" }, StringSplitOptions.None);
            string allLines = string.Join("\n", lines);

            lock (WriteToProcessLock)
            {
                try
                {
                    ChooseDocumentAndProject(doc);
                    
                    Logger.PrintLineWithTime("Sending document: " + doc.Name);
                    ToProcess.WriteLine("EditDocument " + lines.Length);
                    ToProcess.WriteLine(allLines);
                    //ReadAllLines();
                    Logger.PrintLineWithTime("Sent document: " + doc.Name);
                }
                catch (ArgumentException)
                {
                    // Oh, well. If the resource was invalid, then it's probably
                    //  sent again.
                    //throw new Exception();
                }
            }
        }

        public void SendDocumentAsync(Document doc)
        {
            // ... want this to be low-priority... not sure what the best way to do that is
            Task.Factory.StartNew(() => SendDocument(doc), TaskCreationOptions.LongRunning);
        }

        public ITextUndoHistory CurrentUndoHistory { get; private set; }

        private Document currentVisibleDocument;
        private void OnBeforeDocumentWindowShow(Document document, int isFirstShow, IVsWindowFrame vsWindowFrame)
        {
            UpdateCurrentDocument(document, true);
        }

        internal void UpdateCurrentDocument(Document document, bool send = false)
        {
            if (document != null)
            {
                // When switching away from a document, send it.
                if (send
                    && currentVisibleDocument != null
                    && !currentVisibleDocument.FullName.Equals(document.FullName))
                {
                    this.SendDocumentAsync(currentVisibleDocument);
                }

                ITextUndoHistory history;
                if (HistoryRegistryUtil.UndoHistoryRegistry.TryGetHistory(
                    document.GetIWpfTextView().TextBuffer,
                    out history))
                {
                    CurrentUndoHistory = history;
                }
                else
                {
                    // XXX TODO
                    CurrentUndoHistory = null; // ?
                }
            }
            currentVisibleDocument = document;
        }

        private void OnAfterDocumentWindowHide(Document document, IVsWindowFrame vsWindowFrame)
        {
            if (document != null)
            {
                this.SendDocumentAsync(document);
            }
        }

        private void OnAfterSave(Document document)
        {
            this.SendDocumentAsync(document);
        }
    }
}
